package xin.ctkqiang.控制器;

import java.util.HashMap;
import java.util.Map;
import java.util.regex.Pattern;

import org.jetbrains.annotations.NotNull;

import xin.ctkqiang.模型.身份证处理模型;
import xin.ctkqiang.视图.身份证识别结果视图;

/**
 * 身份证处理控制器类，主要负责协调身份证图像的处理、OCR 识别、识别结果的解析以及身份证号码的验证等工作。
 * 它将模型层的图像处理和 OCR 识别功能与视图层的结果展示功能相结合，同时依据国家标准 GB11643 - 1999 对中国身份证号码进行合法性验证。
 * 对于身份证号码的验证，该类支持 15 位和 18 位身份证号码，具体验证步骤如下：
 * 1. 长度检查：确保身份证号码的长度为 15 位或者 18 位。
 * 2. 首位检查：由于中国身份证号码的首位不能为 0，所以会对首位进行检查。
 * 3. 格式检查：利用正则表达式来检查身份证号码的基本格式是否符合规则。
 * 4. 日期检查：验证身份证号码中的出生日期是否合法，同时会考虑闰年的情况。
 * 5. 校验位检查（仅针对 18 位身份证）：通过加权因子计算校验位，并与身份证号码的最后一位进行比较。
 * 
 * +-----------------------------------------------+
 * | 18位身份证号码结构说明 |
 * +-----------------------------------------------+
 * | 位置 | 含义 | 示例 |
 * +-----------------------------------------------+
 * | 1-6 | 行政区划代码 | 350125 (福建省福州市永泰县) |
 * | 7-14 | 出生年月日 | 19840828 (1984年08月28日) |
 * | 15-17 | 顺序码 | 775 (同地区同日期的序号) |
 * | 17 | 性别码(顺序码之一)| 5 (奇数男性，偶数女性) |
 * | 18 | 校验码 | 4 (0-9或X的校验位) |
 * +-----------------------------------------------+
 * 
 * 示例解析:
 * ┌─────────────────────────────────────────────┐
 * │ 350125 19840828 775 4 │
 * │ └──┬──┘ └───┬───┘ └┬┘ └┐ │
 * │ 地区码 出生日期 序号 校验码 │
 * │ └┐ │
 * │ 性别(5:男) │
 * └─────────────────────────────────────────────┘
 */
public class 身份证处理控制器 {
    /**
     * 地区代码表，用于存储中国各地区的行政区划代码及其对应的地区名称。
     * 该表采用 HashMap 数据结构，其中：
     * - 键（Key）：6 位数字的行政区划代码，如 "110000" 表示北京市
     * - 值（Value）：对应的地区名称，如 "北京市"
     * 在解析身份证号码时，通过前 6 位查询该表可获得持证人的籍贯所在地信息。
     */
    private static final Map<String, String> 地区代码表 = new HashMap<>();

    /**
     * 身份证号码加权因子，用于计算校验位。
     * 在国家标准 GB11643 - 1999 中明确规定了这些因子，每个因子对应身份证号码前 17 位中的一位。
     * 在后续校验位的计算过程中，会使用这些因子进行加权求和操作，以此保证校验位的准确性和唯一性。
     */
    private static final int[] 加权因子 = { 7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2 };

    /**
     * 身份证号码校验码对应表。
     * 该表定义了根据加权和取模 11 后得到的余数所对应的校验码字符。
     * 在验证 18 位身份证号码时，会先计算得到余数，然后从该表中查找对应的校验码，
     * 最后将其与身份证号码的最后一位进行比对，从而判断身份证号码的有效性。
     */
    private static final char[] 校验码表 = { '1', '0', 'X', '9', '8', '7', '6', '5', '4', '3', '2' };

    /**
     * 负责身份证图像的处理和 OCR 识别的模型对象。
     * 该对象具备对身份证图像进行预处理以及执行 OCR 识别的功能，为后续的结果解析和验证提供基础数据。
     */
    private 身份证处理模型 模型;

    /**
     * 负责展示 OCR 识别结果的视图对象。
     * 该对象能够将经过处理和验证后的身份证识别结果以直观的方式展示给用户。
     */
    private 身份证识别结果视图 视图;

    static {
        try {
            /**
             * 华北地区行政区划代码
             */
            地区代码表.put("110000", "北京市");
            地区代码表.put("120000", "天津市");
            地区代码表.put("130000", "河北省");
            地区代码表.put("140000", "山西省");
            地区代码表.put("150000", "内蒙古自治区");

            /**
             * 东北地区行政区划代码
             */
            地区代码表.put("210000", "辽宁省");
            地区代码表.put("220000", "吉林省");
            地区代码表.put("230000", "黑龙江省");

            /**
             * 华东地区行政区划代码
             */
            地区代码表.put("310000", "上海市");
            地区代码表.put("320000", "江苏省");
            地区代码表.put("330000", "浙江省");
            地区代码表.put("340000", "安徽省");
            地区代码表.put("350000", "福建省");
            地区代码表.put("360000", "江西省");
            地区代码表.put("370000", "山东省");

            /**
             * 华中地区行政区划代码
             */
            地区代码表.put("410000", "河南省");
            地区代码表.put("420000", "湖北省");
            地区代码表.put("430000", "湖南省");

            /**
             * 华南地区行政区划代码
             */
            地区代码表.put("440000", "广东省");
            地区代码表.put("450000", "广西壮族自治区");
            地区代码表.put("460000", "海南省");

            /**
             * 西南地区行政区划代码
             */
            地区代码表.put("500000", "重庆市");
            地区代码表.put("510000", "四川省");
            地区代码表.put("520000", "贵州省");
            地区代码表.put("530000", "云南省");
            地区代码表.put("540000", "西藏自治区");

            /**
             * 西北地区行政区划代码
             */
            地区代码表.put("610000", "陕西省");
            地区代码表.put("620000", "甘肃省");
            地区代码表.put("630000", "青海省");
            地区代码表.put("640000", "宁夏回族自治区");
            地区代码表.put("650000", "新疆维吾尔自治区");

            /**
             * 特别行政区行政区划代码
             */
            地区代码表.put("810000", "香港特别行政区");
            地区代码表.put("820000", "澳门特别行政区");

            /**
             * 具体地区行政区划代码
             */
            地区代码表.put("350125", "福建省福州市永泰县");
            地区代码表.put("130982", "河北省沧州市任丘市");
            地区代码表.put("441721", "广东省阳江市阳西县");
            地区代码表.put("210504", "辽宁省本溪市市辖区明山区");
            地区代码表.put("231201", "黑龙江省绥化市市辖区");
            地区代码表.put("362424", "江西省抚州地区南丰县");

            /**
             * 江西省地级市行政区划代码
             */
            地区代码表.put("360100", "江西省南昌市");
            地区代码表.put("360200", "江西省景德镇市");
            地区代码表.put("360300", "江西省萍乡市");
            地区代码表.put("360400", "江西省九江市");
            地区代码表.put("360500", "江西省新余市");
            地区代码表.put("360600", "江西省鹰潭市");
            地区代码表.put("360700", "江西省赣州市");
            地区代码表.put("360800", "江西省吉安市");
            地区代码表.put("360900", "江西省宜春市");
            地区代码表.put("361000", "江西省抚州市");
            地区代码表.put("361100", "江西省上饶市");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 解析身份证号码的详细信息。
     * 该方法将身份证号码拆分为地区、出生日期、顺序码和性别等信息。
     * 
     * @param 身份证号码 待解析的身份证号码
     * @return 解析后的详细信息字符串
     */
    public String 解析身份证信息(String 身份证号码) {
        if (!已验证(身份证号码)) {
            return "无效的身份证号码";
        }

        StringBuilder 信息 = new StringBuilder();
        信息.append("身份证信息解析结果：\n");

        String 地区代码 = 身份证号码.substring(0, 6);
        信息.append("地区信息: ").append(地区代码表.getOrDefault(地区代码, "未知地区"))
                .append(" (").append(地区代码).append(")\n");

        String 年 = 身份证号码.substring(6, 10);
        String 月 = 身份证号码.substring(10, 12);
        String 日 = 身份证号码.substring(12, 14);
        信息.append("出生日期: ").append(年).append("年")
                .append(月).append("月")
                .append(日).append("日\n");

        int 性别码 = Integer.parseInt(身份证号码.substring(16, 17));
        String 性别 = (性别码 % 2 == 0) ? "女" : "男";
        信息.append("性别: ").append(性别).append("\n");

        if (身份证号码.length() == 18) {
            信息.append("校验码: ").append(身份证号码.charAt(17)).append("\n");
        }

        return 信息.toString();
    }

    /**
     * 控制器类的构造函数，用于初始化模型和视图对象。
     * 此构造函数的作用是将模型层和视图层的对象注入到控制器中，使得控制器可以协调它们之间的工作，
     * 从而完成身份证图像的处理、识别、解析和验证的一系列操作。
     * 
     * @param 模型 负责身份证图像的处理和 OCR 识别的模型对象，不能为 null。
     * @param 视图 负责展示 OCR 识别结果的视图对象，不能为 null。
     * @throws IllegalArgumentException 如果传入的模型或视图对象为 null，则抛出该异常。
     */
    public 身份证处理控制器(@NotNull 身份证处理模型 模型, @NotNull 身份证识别结果视图 视图) {
        /**
         * 检查传入的模型和视图对象是否为 null，如果为 null 则抛出异常
         */
        if (模型 == null || 视图 == null) {
            throw new IllegalArgumentException("模型和视图对象不能为 null");
        }
        /**
         * 初始化模型和视图对象
         */
        this.模型 = 模型;
        this.视图 = 视图;
    }

    /**
     * 解析 OCR 识别结果并提取身份证号码。
     * 该方法首先会对 OCR 识别得到的原始文本结果进行清理，去除其中多余的空白字符，
     * 然后使用正则表达式在清理后的文本中查找 18 位的身份证号码。
     * 
     * @param OCR结果 OCR 识别的原始文本结果。
     * @return 提取的身份证号码，如果未找到则返回 "未找到身份证号码"。
     */
    private String 解析OCR结果(String OCR结果) {
        /**
         * 去除多余的空白字符，并将连续的空白字符替换为单个空格，最后去除首尾的空白
         */
        String 清理后结果 = OCR结果.replaceAll("\\s+", " ").trim();
        /**
         * 记录清理后的 OCR 结果到日志
         */
        日志记录器.信息("\n清理后的 OCR 结果: " + 清理后结果 + "\n");

        /**
         * 调用查找匹配方法，查找 18 位的身份证号码
         */
        String 身份证号 = 查找匹配(清理后结果, "\\d{18}");
        /**
         * 判断是否找到身份证号码，若未找到则返回提示信息
         */
        return 身份证号.equals("未找到") ? "未找到身份证号码" : 身份证号;
    }

    /**
     * 在文本中查找符合指定模式的内容。
     * 该方法利用正则表达式对输入的文本进行匹配操作，查找第一个符合指定模式的子字符串。
     * 
     * @param 文本 待搜索的文本。
     * @param 模式 正则表达式模式。
     * @return 匹配到的文本，如果未找到则返回 "未找到"。
     */
    private String 查找匹配(String 文本, String 模式) {
        /**
         * 编译正则表达式模式
         */
        Pattern 正则模式 = Pattern.compile(模式);
        /**
         * 创建匹配器对象
         */
        java.util.regex.Matcher 匹配器 = 正则模式.matcher(文本);
        /**
         * 判断是否找到匹配项，若找到则返回匹配内容，否则返回 "未找到"
         */
        return 匹配器.find() ? 匹配器.group() : "未找到";
    }

    /**
     * 处理身份证图像并显示识别结果。
     * 该方法接收身份证图像的路径作为参数，首先调用模型对象的处理图像方法对图像进行处理，
     * 然后对处理后的图像进行 OCR 识别，接着解析识别结果，最后将解析后的结果和验证结果传递给视图对象进行展示。
     * 
     * @param 图像路径 身份证图像的文件路径，不能为 null。
     * @throws IllegalArgumentException 如果传入的图像路径为 null，则抛出该异常。
     */
    public void 处理并显示结果(@NotNull String 图像路径) {
        /**
         * 检查传入的图像路径是否为 null，如果为 null 则抛出异常
         */
        if (图像路径 == null) {
            throw new IllegalArgumentException("图像路径不能为 null");
        }

        /**
         * 调用模型对象的处理图像方法，得到处理后的图像路径
         */
        String 处理后图像路径 = 模型.处理图像(图像路径);

        /**
         * 判断处理后的图像路径是否为 null，如果不为 null 则继续后续操作
         */
        if (处理后图像路径 != null) {
            String 识别结果 = 模型.执行OCR(处理后图像路径);
            String 格式化结果 = this.解析OCR结果(识别结果);

            if (!格式化结果.equals("未找到身份证号码")) {
                String 身份证信息 = this.解析身份证信息(格式化结果);
                视图.显示识别结果(身份证信息);
            } else {
                视图.显示识别结果(格式化结果);
            }
        }
    }

    /**
     * 验证身份证号码的合法性。
     * 该方法支持对 15 位和 18 位身份证号码进行验证。首先会进行基本的空值、长度和首位检查，
     * 然后根据身份证号码的长度调用相应的验证方法进行进一步验证。
     * 
     * @param 身份证号码 待验证的身份证号码，不能为 null。
     * @return 验证结果：true 表示合法，false 表示非法。
     * @throws IllegalArgumentException 如果传入的身份证号码为 null，则抛出该异常。
     */
    public boolean 已验证(@NotNull String 身份证号码) {
        /**
         * 检查传入的身份证号码是否为 null，如果为 null 则抛出异常
         */
        if (身份证号码 == null) {
            throw new IllegalArgumentException("身份证号码不能为 null");
        }
        /**
         * 检查身份证号码是否为空，如果为空则返回 false
         */
        if (身份证号码.isEmpty()) {
            return false;
        }

        /**
         * 检查身份证号码的长度是否为 15 位或 18 位，如果不是则记录警告信息并返回 false
         */
        if (身份证号码.length() != 18 && 身份证号码.length() != 15) {
            日志记录器.警告("身份证号码长度不正确，应为 18 或 15 位。");
            return false;
        }

        /**
         * 检查身份证号码的首位是否为 0，如果为 0 则记录警告信息并返回 false
         */
        if (身份证号码.charAt(0) == '0') {
            日志记录器.警告("中国身份证号码首位不能为 0。你确定这位先生/女士是中国公民吗？");
            return false;
        }

        /**
         * 根据身份证号码的长度调用相应的验证方法
         */
        return 身份证号码.length() == 18 ? 验证18位身份证(身份证号码) : 验证15位身份证(身份证号码);
    }

    /**
     * 验证 18 位身份证号码。
     * 该方法首先使用正则表达式检查 18 位身份证号码的基本格式是否正确，
     * 然后将身份证号码转换为大写，接着计算前 17 位的加权和，
     * 根据加权和取模 11 得到校验位索引，从校验码表中查找对应的校验码，
     * 最后将计算得到的校验码与身份证号码的最后一位进行比较，以判断其合法性。
     * 
     * @param 身份证号 18 位身份证号码。
     * @return 验证结果：true 表示合法，false 表示非法。
     */
    private boolean 验证18位身份证(String 身份证号) {
        /**
         * 使用正则表达式检查 18 位身份证号码的基本格式，如果格式不正确则记录警告信息并返回 false
         */
        if (!Pattern.matches("^[1-9]\\d{5}(18|19|20)\\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\\d|3[01])\\d{3}[0-9Xx]$",
                身份证号)) {
            日志记录器.警告("18 位身份证号码格式不正确。");
            return false;
        }
        /**
         * 将身份证号码转换为大写，方便后续处理
         */
        身份证号 = 身份证号.toUpperCase();

        int 总和 = 0;
        /**
         * 计算前 17 位的加权和
         */
        for (int i = 0; i < 17; i++) {
            总和 += (身份证号.charAt(i) - '0') * 加权因子[i];
        }

        /**
         * 计算加权和取模 11 的结果，得到校验位索引
         */
        int 校验位索引 = 总和 % 11;
        /**
         * 从校验码表中查找对应的校验码
         */
        char 计算校验码 = 校验码表[校验位索引];

        /**
         * 比较计算得到的校验码与身份证号码的最后一位，如果不相等则记录警告信息并返回 false
         */
        if (计算校验码 != 身份证号.charAt(17)) {
            日志记录器.警告("18 位身份证号码校验位不正确。");
            return false;
        }

        /**
         * 所有检查都通过，返回 true 表示身份证号码合法
         */
        return true;
    }

    /**
     * 验证 15 位身份证号码。
     * 该方法首先使用正则表达式检查 15 位身份证号码的基本格式是否正确，
     * 然后提取其中的出生日期信息，调用日期合法性验证方法进行验证，以此判断身份证号码的合法性。
     * 
     * @param 身份证号 15 位身份证号码。
     * @return 验证结果：true 表示合法，false 表示非法。
     */
    private boolean 验证15位身份证(String 身份证号) {
        /**
         * 使用正则表达式检查 15 位身份证号码的基本格式，如果格式不正确则记录警告信息并返回 false
         */
        if (!Pattern.matches("^[1-9]\\d{5}\\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\\d|3[01])\\d{3}$", 身份证号)) {
            日志记录器.警告("15 位身份证号码格式不正确。");
            return false;
        }

        /**
         * 提取出生日期信息
         */
        int 年份 = Integer.parseInt(身份证号.substring(6, 8));
        int 月份 = Integer.parseInt(身份证号.substring(8, 10));
        int 日期 = Integer.parseInt(身份证号.substring(10, 12));

        /**
         * 验证出生日期的合法性，如果不合法则记录警告信息并返回 false
         */
        if (!验证日期合法性(1900 + 年份, 月份, 日期)) {
            日志记录器.警告("15 位身份证号码中的出生日期不合法。");
            return false;
        }

        /**
         * 所有检查都通过，返回 true 表示身份证号码合法
         */
        return true;
    }

    /**
     * 验证日期的合法性。
     * 该方法首先检查月份是否在 1 到 12 之间，然后根据年份判断是否为闰年，
     * 若为闰年则调整 2 月的天数，最后检查日期是否在该月的有效范围内，以此判断日期是否合法。
     * 
     * @param 年 年份。
     * @param 月 月份。
     * @param 日 日期。
     * @return 验证结果：true 表示合法，false 表示非法。
     */
    private boolean 验证日期合法性(int 年, int 月, int 日) {
        /**
         * 检查月份是否在有效范围内，如果不在则返回 false
         */
        if (月 < 1 || 月 > 12) {
            return false;
        }

        /**
         * 定义每个月的天数，默认 2 月为 28 天
         */
        int[] 每月天数 = { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };

        /**
         * 判断是否为闰年，如果是闰年则 2 月有 29 天
         */
        if ((年 % 4 == 0 && 年 % 100 != 0) || (年 % 400 == 0)) {
            每月天数[1] = 29;
        }

        /**
         * 检查日期是否在该月的有效范围内，如果不在则返回 false
         */
        return 日 >= 1 && 日 <= 每月天数[月 - 1];
    }
}